16.1 概述
在深入内存分配算法细节前,我们需要了解一些基本概念,这有助于建立宏观认识。
基本策略:
1.每次从操作系统申请一大块内存(比如1MB),以减少系统调用。
2.将申请到的大块内存按照特定大小预先切分成小块,构成链表。
3.为对象分配内存时,只须从大小合适的链表提取一个小块即可。
4.回收对象内存时,将该小块内存重新归还到原链表,以便复用。
5.如闲置内存过多,则尝试归还部分内存给操作系统,降低整体开销。
内存分配器只管理内存块,并不关心对象状态。且它不会主动回收内存,垃圾回收器在完成清理操作后,触发内存分配器的回收操作。
内存块
分配器将其管理的内存块分为两种。
- span:由多个地址连续的页(page)组成的大块内存。
- object:将span按特定大小切分成多个小块,每个小块可存储一个对象。
按照其用途,span面向内部管理,object面向对象分配。
分配器按页数来区分不同大小的span。比如,以页数为单位将span存放到管理数组中,需要时就以页数为索引进行查找。当然,span大小并非固定不变。在获取闲置span时,如果没找到大小合适的,那就返回页数更多的,此时会引发裁剪操作,多余部分将构成新的span被放回管理数组。分配器还会尝试将地址相邻的空闲span合并,以构建更大的内存块,减少碎片,提供更灵活的分配策略。
malloc.go
_PageShift=13 _PageSize =1<< _PageShift //8KB
mheap.go
type mspan struct{
next *mspan // 双向链表
prev *mspan
start pageID // 起始序号 = (address>> _PageShift)
npages uintptr // 页数
freelist gclinkptr // 待分配的object链表
}
用于存储对象的object,按8字节倍数分为n种。比如说,大小为24的object可用来存储范围在17~24字节的对象。这种方式虽然会造成一些内存浪费,但分配器只须面对有限几种规格(size class)的小块内存,优化了分配和复用管理策略。
分配器会尝试将多个微小对象组合到一个object块内,以节约内存。
malloc.go
_NumSizeClasses=67
分配器初始化时,会构建对照表存储大小和规格的对应关系,包括用来切分的span页数。
msize.go
//Size classes. Computed and initialized by InitSizes. // //SizeToClass(0⇐n⇐MaxSmallSize)returns the size class, // 1⇐sizeclass<NumSizeClasses,for n. // Size class 0 is reserved to mean”not small”. // //class_to_size[i] =largest size in class i //class_to_allocnpages[i] =number of pages to allocate when // making new objects in class i
var class_to_size[_NumSizeClasses]int32 var class_to_allocnpages[_NumSizeClasses]int32
var size_to_class8[1024/8+1]int8 var size_to_class128[(_MaxSmallSize-1024)/128+1]int8
若对象大小超出特定阈值限制,会被当作大对象(large object)特别对待。
malloc.go
_MaxSmallSize=32<<10 //32KB
管理组件
优秀的内存分配器必须要在性能和内存利用率之间做到平衡。好在Go的起点很高,直接采用了tcmalloc的成熟架构。
malloc.go
//Memory allocator,based on tcmalloc. //http://goog-perftools.sourceforge.net/doc/tcmalloc.html
分配器由三种组件组成。
- cache:每个运行期工作线程都会绑定一个cache,用于无锁object分配。
- central:为所有cache提供切分好的后备span资源。
- heap:管理闲置span,需要时向操作系统申请新内存。
mheap.go
type mheap struct{ free [_MaxMHeapList]mspan // 页数在127以内的闲置span链表数组 freelarge mspan // 页数大于127(>=1MB) 的大span链表
// 每个central对应一种sizeclass central[_NumSizeClasses]struct{ mcentral mcentral } }
mcentral.go
type mcentral struct{ sizeclass int32 // 规格 nonempty mspan // 链表:尚有空闲object的span empty mspan // 链表:没有空闲object,或已被cache取走的span }
mcache.go
type mcache struct{ alloc[_NumSizeClasses]*mspan // 以sizeclass为索引管理多个用于分配的span }
分配流程:
1.计算待分配对象对应的规格(size class)。
2.从cache.alloc数组找到规格相同的span。
3.从span.freelist链表提取可用object。
4.如span.freelist为空,从central获取新span。
5.如central.nonempty为空,从heap.free/freelarge获取,并切分成object链表。
6.如heap没有大小合适的闲置span,向操作系统申请新内存块。
释放流程:
1.将标记为可回收的object交还给所属span.freelist。
2.该span被放回central,可供任意cache重新获取使用。
3.如span已收回全部object,则将其交还给heap,以便重新切分复用。
4.定期扫描heap里长时间闲置的span,释放其占用的内存。
注:以上不包括大对象,它直接从heap分配和回收。
作为工作线程私有且不被共享的cache是实现高性能无锁分配的核心,而central的作用是在多个cache间提高object利用率,避免内存浪费。
假如cache1获取一个span后,仅使用了一部分object,那么剩余空间就可能会被浪费。而回收操作将该span交还给central后,该span完全可以被cache2、cacheN获取使用。此时,cache1已不再持有该span,完全不会造成问题。
将span归还给heap,是为了在不同规格object需求间平衡。
某时段某种规格的object需求量可能激增,那么当需求过后,大量被切分成该规格的span就会被闲置浪费。将其归还给heap,就可被其他需求获取,重新切分。